use std::future::Future;
use std::sync::Arc;

use ecksport_rpc::prelude::*;
use futures::TryFutureExt;
use tracing::*;

use aspect_codec::ecksport::AsAspCodec;
use aspect_db_common::device::DirEntry;
use aspect_ident::{DeviceIdent, DirPackage};
use aspect_proto::{
    errors::AspProtoError,
    identity::{self, AnnRequest, AnnResponse, QueryRequest, QueryResponse},
};

use crate::storage::{HomeserverStorage, StorageError};

pub struct IdentityServer {
    storage: Arc<HomeserverStorage>,
}

impl IdentityServer {
    pub fn new(storage: Arc<HomeserverStorage>) -> Arc<Self> {
        Arc::new(Self { storage })
    }

    async fn get_device_entry(&self, id: DeviceIdent) -> Result<Option<DirEntry>, AspProtoError> {
        Ok(self
            .storage
            .query_dir_entry(&id)
            .map_err(conv_storage_err)
            .await?)
    }

    async fn insert_dir_package(&self, pkg: DirPackage) -> Result<(), AspProtoError> {
        let _ = self
            .storage
            .try_accept_dir_package(pkg)
            .map_err(conv_storage_err)
            .await?;
        Ok(())
    }

    async fn list_user_device_ids(&self, user: &str) -> Result<Vec<DeviceIdent>, AspProtoError> {
        Ok(self
            .storage
            .get_user_device_ids(user)
            .map_err(conv_storage_err)
            .await?)
    }
}

fn conv_storage_err(e: StorageError) -> AspProtoError {
    e.into()
}

impl aspect_proto::core::IdentServ for IdentityServer {
    fn ident_query(
        &self,
        _pdata: PeerData,
        query: AsAspCodec<identity::QueryRequest>,
    ) -> impl std::future::Future<Output = RpcResult<AsAspCodec<identity::QueryResponse>>> + Send
    {
        async move {
            match query.inner() {
                QueryRequest::Id(id) => {
                    match self.get_device_entry(*id).await? {
                        Some(DirEntry::Pkg(pkg)) => Ok(QueryResponse::new(vec![pkg]).into()),
                        Some(DirEntry::Equiv(_eqv)) => {
                            // TODO include the report in the response
                            warn!(%id, "got request for equived device");
                            Ok(QueryResponse::new(Vec::new()).into())
                        }
                        None => Ok(QueryResponse::new(Vec::new()).into()),
                    }
                }

                QueryRequest::Dump(_) => {
                    let idents = self
                        .storage
                        .dump_devices()
                        .map_err(conv_storage_err)
                        .await?;
                    Ok(QueryResponse::new(idents).into())
                }

                QueryRequest::User(query_username) => {
                    let ids = self.list_user_device_ids(query_username).await?;
                    trace!(%query_username, "queried devices");
                    let mut devices = Vec::new();
                    for ident in ids {
                        match self.get_device_entry(ident).await? {
                            Some(DirEntry::Pkg(pkg)) => {
                                devices.push(pkg);
                            }
                            Some(_) => {
                                warn!(%query_username, "ignoring eqv in user query");
                            }
                            None => {
                                warn!(%query_username, %ident, "found listing for device in user but missing from database");
                            }
                        }
                    }

                    trace!(%query_username, devices = %devices.len(), "queried user devices");
                    Ok(QueryResponse::new(devices).into())
                }
            }
        }
    }

    fn ident_announce(
        &self,
        _pdata: PeerData,
        ident: AsAspCodec<DirPackage>,
    ) -> impl std::future::Future<Output = RpcResult<AsAspCodec<identity::AnnResponse>>> + Send
    {
        async move {
            let pkg = ident.into_inner();
            let seqno = pkg.seqno();
            let ident = *pkg.last_update().ident();
            self.insert_dir_package(pkg).await?;
            info!(%ident, %seqno, "accepted DIR");
            Ok(AnnResponse::Accepted(()).into())
        }
    }
}
